X

曜彤.手记

随记,关于互联网技术、产品与创业

《深入理解 C++11:C++11 新特性解析与应用》读书笔记(一)

作为一本之前已经读过两遍的书,终于决定在第三次“复习”的时候做下读书笔记了。鉴于之前已经完整读过《Primer C++ 5th》、《Effective C++ 3th》两本书,因此本文仅作为查缺补漏之用,对于前两本书中没有提到一些诸如“最小垃圾回收”、以及“原子类型与原子操作”等内容进行回顾与记录。整个 C++ 系列还有一本想完整仔细阅读的《Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14》可能会稍微往后放了,由于 C++14 仅作为 C++11 的微小改进和补充,对于一些常用的特性其实已经在实际项目中开始使用了。而对于诸如 std::future 以及 std::promise 等特性,由于其涉及异步和并发相关场景,因此可以参考《C++ Concurrency in Action 2th》一书,暂时没有实际需求便先不打算进行了解。

  1. Page 20C++98/03 标准:C++03 标准产生于2003年 WG21 提交的 TC1 技术勘误表,对语言核心内容没有改动。因此常被合在一起称为 C++98/03 标准。
  2. Page 21WG21:C++ 语言标准委员会;WG14:C 语言标准委员会。其中 WG21 更倾向于使用库而不是扩展语言来实现新的 C++ 特性
  3. Page 31final 与 override 为标识符,并非关键字,因此可以被当做变量名进行重定义。
  4. Page 33const 与 constexpr 区别:const 不一定保证编译时的常量性,只保证运行时变量无法变更;而 constexpr 则可以保证编译时的常量性。
  5. Page 39一些用于检查机器对 C 标准以及 C 库支持情况的预定义宏,使用前先检查是否被定义(#ifdef):
int main(int argc, char** argv) {
  std::cout << __STDC_HOSTED__ << std::endl;  // 是否包含完整的标准 C 库;
  std::cout << __STDC__ << std::endl;  // 实现是否与 C 标准一致;
  std::cout << __STDC_VERSION__ << std::endl; // 支持的 C 标准版本;
  std::cout << __STDC_ISO_10646__ << std::endl; // 表示 C++ 编译环境符合某个版本的 IOS/IEC 10646 标准(通用字符集)版本;
  return 0;
}
  1. Page 40在 C++11 中,宏 __func__ 可返回所在函数/类的名字,可用于轻量级的代码调试。实现上编译器会自动隐式地在函数定义之后定义 func 标识符。
  2. Page 41在 C++11 中,宏 _Pragma 的用法类似 #pragma,用于向编译器传达语言标准以外的信息。但作为操作符,其可以用于宏定义并进行宏展开。
// 示例来源于 OpenMP 应用(一套支持跨平台共享内存方式的多线程并发的编程API);
#define Pragma(x) _Pragma(#x)
#define OMP(directive) Pragma(omp directive)

int main(int argc, char** argv) {
  omp_set_dynamic(0);
  omp_set_num_threads(2);
  OMP(parallel) {  // 其内部的语句将被多个线程并行执行;
    printf("Hello!\n");
  }
}
  1. Page 42变长参数宏与 std::fprintf:
#include <cstdio>
#define LOG(...) { \
  fprintf(stderr, "%s: Line %d:\t", __FILE__, __LINE__); \
  fprintf(stderr, __VA_ARGS__); \
  fprintf(stderr, "\n"); \
}
int main() {
  int x = 3;
  LOG("x = %d", x);  // "x.cpp: Line 12: x = 3"
}
  1. Page 43C++ 标准规定 long long 至少有64位长度。
  2. Page 44强类型的语言遇到函数引数类型和实际调用类型不符合的情况经常会直接出错或者编译失败;而弱类型的语言常常会实行隐式转换,或者产生难以意料的结果。所以从这方面来看,C/C++ 是一种弱类型语言
  3. Page 45数据类型的 rank 相同时,一般按照低等级整型转换为高等级整型,有符号的转换为无符号
  4. Page 46使用 __cplusplus 宏判断编译使用的 C++ 版本:
#if __cplusplus < 201103L
  #error "should use C++11 implementaton."
#endif
  1. Page 51如上一条所示,#error 宏为预处理时的”断言“;而 static_assert() 为编译时静态断言,可达到率百分之百。其中使用的表达式必须为常量表达式;assert() 为动态运行时断言,只能断言到被运行到的代码块。
  2. Page 59在 C++11 中,可以使用 sizeof 对类成员表达式(非类实例成员)进行操作。
  3. Page 61形如 friend int; 在内置类型上的友元声明一般会被编译器忽略,因此这对于模板友元是一个方便的地方。
  4. Page 65final 和 override 关键字:
struct A {
  virtual void foo() {
    std::cout << "A" << std::endl;
  }
};
struct B : public A {
  void foo() final override {
    std::cout << "B" << std::endl;
  }
};
int main(int argc, char** argv) {
  A a, *fa = &a;
  B b, *fb = &b;
  fb->foo();  // 动态调用,若派生类没实现,则调用基类的同名同参虚函数,使用 final 可以防止虚函数被派生类复写;
  return 0;
}
  1. Page 67override 关键字可以帮助开发者确认被标记的函数正确地重载了其父类中的虚函数,而非想要新添加成员函数。这在多继承或派生类继承链较深的情况下十分有帮助。
  2. Page 68模板参数对于非引用类型,在推导时会丢失 top-level 常量性。若为引用类型,当模板参数为 “&&” 右值引用时可以保持引用的左值性和常量性。其中对于 T,仅能推导出原类型或者对应的左值引用类型。
template<typename T>
void ftVal(T t) {
  std::cout << typeid(T).name() << std::endl;
}
template<typename T>
void ftRef(T&& t) {
  std::cout << typeid(T).name() << std::endl;
}
int main(int argc, char** argv) {
  const int vInt = 10;
  const int&& vIntRR = 100;
  const char* const vChar = "Hello, world!";
  auto& vIntRef = vInt;
  ftVal(vInt);  // "i";
  ftVal(vChar);  // "PKc";
  ftRef(vIntRR); // T = const int&;
  ftRef(100); // T = int;
  ftRef('c'); // T = char;
  ftRef(vInt); // T = const int&;
  ftRef(vChar); // T = const char* const&;
  ftRef(vIntRef); // T = const int&;
  return 0;
}
  1. Page 72外部(extern)模板声明可以放置在头文件中以便于使用。
  2. Page 73一般来说,外部模板声明可用于优化编译及链接时间,建议仅在项目比较大的情况下再使用。对于大部分正常的模板实例化,编译器已经会进行一定程度的冗余实例优化。
  3. Page 74接受匿名和局部类型的模板:
template<typename T>
void foo(T t) {}
struct {} so;
int main(int argc, char** argv) {
  struct {} si;
  foo(si);
  foo(so);
  return 0;
}
  1. Page 78根据 C++ 名字查找规则,派生类中的同名成员函数会覆盖基类中的函数,而不能跨作用域进行重载。但经过 using 改变作用域可见性后,在派生类中便可以与基类中同名成员函数组成重载关系。
struct A {
  void foo() { std::cout << 'A' << std::endl; }
};
struct B : public A {
  using A::foo;
  void foo(int x) { std::cout << 'B' << x << std::endl; }
};
int main(int argc, char** argv) {
  B b;
  b.foo();
  b.foo(100);
  return 0;
}
  1. Page 80在派生类中发生冲突的多继承类构造函数,需要被单独定义。
  2. Page 81继承的构造函数不受 using 可见性的影响。
struct A {
  int v;
  A(int v) : v(v) {}
};
class B : public A {
  using A::A;
};
int main(int argc, char** argv) {
  B b(100);
  return 0;
}
  1. Page 83委派构造函数(调用其他构造函数进行初始化)不能有初始值列表,即构造函数不能同时“委派”和使用初值列表。对于剩余的初始化操作,只能在构造函数的函数体中进行。
struct A {
  int x;
  A() {}
  // A(int x) : A(), x(x) {}  // wrong!
};
  1. Page 84由于目标构造函数的执行总是优先于委派构造函数,因此避免目标构造函数和委托构造函数中初始化相同的成员通常是必要的。
  2. Page 85基于委派构造,使用构造模板函数产生目标的泛型构造函数:
struct A {
  std::list<int> lst;
  template<typename T>
  A(T start, T end) : lst(start, end) {}
};
int main(int argc, char** argv) {
  std::vector<int> v = {1, 2, 3};
  A a(v.begin(), v.end());
  for (auto e : a.lst) { std::cout << e << std::endl;    }
  return 0;
}
  1. Page 94将亡值:std::move 的返回值、类型为 T&& 将要被移动的对象(被右值引用类型变量标记的右值);纯右值:返回的临时变量值、字面量值、lambda 表达式、运算表达式、类型转换函数的返回值。
  2. Page 95左值引用是具名变量值的别名,右值引用是不具名(匿名)变量的别名。
  3. Page 101移动构造的常用方式:
struct A {
  A() = default;
  A(int v) : p(new int(v)) {}
  ~A() { delete p; }
  A(const A&) = delete;
  A& operator=(const A&) = delete;
  A(A&& rhs) noexcept : p(rhs.p) { rhs.p = nullptr; }
  A& operator=(A&& rhs) noexcept { p = rhs.p; rhs.p = nullptr; return *this; }
  int getPV() const { return *p; }
 private:
  int* p;
};
A getTempA(int v) { return A(v); }
int main(int argc, char** argv) {
  auto a = getTempA(100);  // 自 C++17 起,某些 RVO/NRVO 类似的临时值消除过程会由编译器强制执行,而不受 -fno-elide-constructors 参数的影响;
  std::cout << a.getPV() << std::endl;
  return 0;
}
  1. Page 103标准库 STL 中的一些容器类型只会使用被标记为不会抛出异常的移动构造和移动赋值函数。std::move_if_noexcept()
  2. Page 105使用 std::forward 保持模板传递时的参数类型:
void bar(int& v) { std::cout << "int& v" << std::endl; }
void bar(const int& v) { std::cout << "const int& v" << std::endl; }
void bar(int&& v) { std::cout << "int&& v" << std::endl; }
void bar(const int&& v) { std::cout << "const int&& v" << std::endl; }
template<typename T>
void foo(T&& t) { bar(std::forward<T>(t)); }
int main(int argc, char** argv) {
  int x = 1;
  const int y = 2;
  int&& z = 3;
  const int&& k = 4;
  foo(x);  // "int& v";
  foo(y);  // "const int& v";
  foo(z);  // "int& v";
  foo(k);  // "const int& v";
  foo(10);  // "int&& v";
  foo(static_cast<const int&&>(10));  // "const int&& v";
  return 0;
}
  1. Page 108利用完美转发做包装函数:
template<typename T, typename U>
void PerfectForward(T&& t, U& f) { f(std::forward<T>(t)); }
  1. Page 109在构造函数有默认参数值的情况下,构造函数仍有可能被隐式调用
struct A {
  A(int x, int y = 10) {}
};
int main(int argc, char** argv) {
  A a = 1;
  return 0;
}
  1. Page 110将 explicit 应用于自定义类型转换操作符以阻止自定义类型的隐式自动转换:
struct A {
  explicit operator bool() { return true; }
};
int main(int argc, char** argv) {
  A x, y;
  // std::cout << x + y << std::endl; // 2;
  return 0;
}
  1. Page 116对于 const 变量来说,如果新类型可以完整存放其值,则通过列表初始化方式赋值时并不会出现 narrowing 问题。
  2. Page 127若非受限联合体有非 POD 成员,且该成员有非平凡的构造函数,则该联合体的默认构造函数/析构函数将被标记为删除,需要进行自定义。
union T {
  std::string s;
  int v;
};
int main(int argc, char** argv) {
  // T t;  // wrong!
  return 0;
}
  1. Page 128pseudo-destructor:伪析构函数需要保证显式调用非类类型的析构函数的语法是有效,因此可以编写代码而不必知道给定类型是否存在析构函数(内置类型 or 自定义类型)。
struct A {
  A() = default;
  ~A() { std::cout << "Real Destructor A." << std::endl; }
};
using TypeA = A;
using TypeInt = int;
int main(int argc, char** argv) {
  TypeA v;
  TypeInt i = 10;
  v.~TypeA();
  i.~TypeInt();
  return 0;
}
  1. Page 131自定义字面量值(operator “” [_Literal]):
struct A {
  int v;
  A(int v) : v(v) {}
};
A operator "" _toA(const unsigned long long v) {
  return A(v);
}
int main(int argc, char** argv) {
  auto t = 10_toA;
  std::cout << t.v << std::endl;
  return 0;
}
  1. Page 132自定义字面量值(operator “”)的参数要求:
  • 整数型:unsigned long long / const char*;
  • 浮点数:long double / const char*;
  • 字符串:const char* + size_t;
  • 字符:char;
  1. Page 140SFINEA 规则:匹配失败不是错误。即对重载的模板参数进行展开的时候,如果展开导致了一些类型不匹配,编译器并不会报错。基于此规则,编译器会对某些模板使用更为精确的版本来实例化,另外一些则使用通用版本进行实例化。
  2. Page 144一般函数内没有声明为 static 的变量总是具有自动存储期的局部变量。
  3. Page 150auto 关键字不能保持变量而非引用的顶层 CV(const/volatile)特性。
int main(int argc, char** argv) {
  int x = 0;
  const int& y = x;
  auto& z = y;  // "const int &z";
  return 0;
}
  1. Page 151auto 实际上是一个将要推导出类型的占位符。
  2. Page 152使用 auto 推导数组类型时需要显式指定类型为指针,否则会退化为指针地址对应的整型。
  3. Page 160decltype(e) 的类型推导规则:
  • 如果 e 是一个没有带括号的标记符(一般为程序员自定义的标记)表达式或类成员访问表达式,则推导结果为 e 所命名的实体类型;
  • 否则,若 e 的类型为 T,若 e 为将亡值 xvalue(一般为 std::move(x);而字面量值通常为纯右值 prvalue),则推导结果为 T&&;
  • 否则,若 e 的类型为 T,若 e 为一个左值(引用),则推导结果为 T&;
  • 否则,若 e 的类型为 T,推导结果为 T;
  1. Page 161注意 decltype 在推导自增运算符表达式时的区别:
int main(int argc, char** argv) {
  int i = 10;
  decltype(i++) x;  // int;
  decltype(++i) x = i;  // "int&";
  return 0;
}
  1. Page 164与 auto 不同的是,放置于 decltype 后面的*号不会被编译器忽略,因此在推导指针类型时不需要另外放置该符号。
  2. Page 165追踪返回类型:
template<typename T1, typename T2>
auto sum(T1& t1, T2& t2) -> decltype(t1 + t2) {
  return t1 + t2;
}
  1. Page 167定义类型:“一个函数返回一个函数指针,这个函数指针的返回值是一个函数指针”:
int(*(*foo)())() {};
auto foo() -> auto (*)() -> int(*)() {};
  1. Page 179非强类型枚举类的缺点:非强类型作用域(污染全局环境)、允许隐式转换为整型、占用存储空间及符号性不确定;
  2. Page 184只能使用右值来初始化一个 std::unique_ptr,而 std::unique_ptr 可以通过 std::move 来交换所有权:
int main(int argc, char** argv) {
  auto up = std::make_unique<int>(10);
  auto nup = std::move(up);
  std::cout << (up == nullptr) << std::endl;  // 1;
  std::cout << *nup << std::endl;  // 10;
  return 0;
}
  1. Page 186由于 std::shared_ptr 控制块堆内存的释放与引用托管对象的 std::weak_ptr 的数量有关。因此在不需要使用 std::weak_ptr 时,可以通过 std::weak_ptr::reset 及时切断引用。
  2. Page 187两种常用的垃圾回收方式:
  • 基于引用计数:优点实现简单,缺点容易造成循环引用(参考 std::shared_ptr);
  • 基于跟踪处理:
    • 标记-清除(Mark-Sweep):查找程序使用对象的堆空间,并做标记,所有被标记的对象便为可达对象,没有被标记的对象将在“清除”步骤被清理。存在内存碎片问题
    • 标记-整理(Mark-Compact):同上,但标记之后不做清理,而是将所有可达对象向内存一端移动,以解决碎片问题;存在需要更新程序中所有堆内存引用的问题
    • 标记-拷贝(Mark-Copy):From-To,现在 From 里分配内存,分配满之后开始垃圾回收,将 From 中所有可达对象向 To 拷贝。然后交换角色重复上述过程。存在堆内存利用率低的问题
  1. Page 189C++ 垃圾回收:贝姆(Boehm)垃圾收集器。
  2. Page 190检查是否支持最小垃圾回收及安全派生指针:
int main(int argc, char** argv) {
  std::cout << "Pointer safety: ";
  switch (std::get_pointer_safety()) {
    case std::pointer_safety::strict: std::cout << "strict\n"; break;
    case std::pointer_safety::preferred: std::cout << "preferred\n"; break;
    case std::pointer_safety::relaxed: std::cout << "relaxed\n"; break;
  }
  return 0;
}
  1. Page 193数组大小参数、switch-case 的 case 语句以及枚举类成员都需要使用编译期常量进行初始化。
  2. Page 195constexpr 函数:
  • 函数体只有单一的 return 语句(可以有 using、typedef 以及 assert);
  • 函数必须有返回值(不能是 void 函数);
  • 函数在使用前必须已有定义;
  • 函数的 return 返回语句中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式;
  1. Page 197const 常量和 constexpr 常量的区别:大多数情况下两者没有区别。但如果在全局命名空间中,编译器一定会为 const 产生数据,而 constexpr 除非有代码显式使用了它的地址,否则一般不会为其生成数据,而仅当做编译期的值(编译时替换),类似枚举值。
  2. Page 198constexpr 构造函数:
  • 函数体必须为空;
  • 初始化列表只能由常量表达式(包括字面量值)来赋值;
class A {
  int i;
 public:
  constexpr A(int i) : i(i) {} 
  constexpr int getV() const { return i; }
};
int main(int argc, char** argv) {
  constexpr int v = 10;
  constexpr A a{v};
  int arr[a.getV()] = {1, 2, 3, 4};
  return 0;
}
  1. Page 199当声明为常量表达式的模板函数后,而某个其实例化结果不满足常量表达式的需求的话,则 constexpr 关键字会被自动忽略。
  2. Page 202constexpr 元编程 / template 元编程,两者均是图灵完备的。
// template - TMP;
template<size_t n>
constexpr int fib() { return fib<n - 1>() + fib<n - 2>() + 1; }
template<> constexpr int fib<0>() { return 1; }
template<> constexpr int fib<1>() { return 1; }

// constexpr - CMP;
constexpr int fib(int i) {
  return i == 1 ? 1 : ((i == 2) ? 1 : fib(i - 1) + fib(i - 2));
}
int main(int argc, char** argv) {
  int arrA[fib(5)] = {0};
  int arrB[fib<3>()] = {0};
  return 0;
}



评论 | Comments


Loading ...